#!/usr/bin/python
"""
pyre.py

CREs <-> Python regular expressions.
"""

import re


class Error(Exception):
  pass


# TODO: These will need to go both ways?  Put it in tables.py.

ZERO = {
    'begin': '^',
    'end': '$',
    'boundary': r'\b',
    }

SINGLE = {
    'any': '.',  # TODO: isn't this gone?

    'hash': '#',
    'bang': '!',

    'space': '[ ]',
    'tab': r'\t',
    'newline': r'\n',
    'cr': r'\r',

    # This is needed only inside character classes.  Outside, just use '-'.  I
    # TODO: I think you need a SINGLE token and a CHARS_SINGLE token?
    'hyphen': r'\-',

    'lbracket': '\[',
    'rbracket': '\]',

    # This is for being INSIDE character class.
    'squote': "'",
    'dquote': '"',
    }

PERL = {
    'digit': r'\d',
    'wordchar': r'\w',
    'whitespace': r'\s',
    }

# left, negated
_ASSERT_CHARS = {
    (False, True ): '=',   # positive lookahead
    (False, False): '!',   # negative lookahead
    (True,  True ): '<=',  # positive lookbehind
    (True,  False): '<!',  # negative lookbehind
    }


def _CharClassEscape(s):
  """
  re.escape is overzealous for stuff in char classes.
  This is purely cosmetic -- makes it easier to debug the compiler by inspecting
  its output.
  """
  escaped = []
  for ch in s:
    # Characters that we know don't need to be escaped inside a char class.
    if ch in '(){}<>:;`.,?+*"\'':
      escaped.append(ch)
    else:
      escaped.append(re.escape(ch))
  return ''.join(escaped)


def convert_flags(expr):
  # TODO!
  if expr:
    return '(?i)'
  else:
    return ''


def gen_re(defs, expr):
  """Compile a dictionary of s-expressions.

  Turn it directly into Python without going through classes?
  """
  assert isinstance(expr, list), expr
  node_type = expr[0]
  assert isinstance(node_type, str), expr

  try:
    # We use 'name' or 'child' depending on whether it's a composite or an atom.
    child = expr[1]  # ab+
    name = expr[1]  # %begin
  except IndexError:
    child = None
    name = None

  # Reference to another definition.
  if node_type == 'Ref':
    try:
      sub_expr = defs[name]
    except KeyError:
      raise Error("Invalid reference to %s in %s" % (name, expr))
    # Expanded inline.
    return gen_re(defs, sub_expr)

  elif node_type == 'Repeat':
    # e.g. ['Repeat', None, (1,3), ['Ref', 'Number']]
    modifier, repeat, child = expr[1:4]

    if repeat in ('+', '*', '?'):
      suffix1 = repeat
    elif isinstance(repeat, int):
      suffix1 = '{%d}' % repeat
    elif isinstance(repeat, tuple):
      start, end = repeat
      assert start is not None or end is not None, (start, end)
      if start is None:
        suffix1 = '{,%d}' % end
      elif end is None:
        suffix1 = '{,%d}' % end
      else:
        assert isinstance(start, int), start
        assert isinstance(end, int), end
        suffix1 = '{%d,%d}' % (start, end)
    else:
      raise AssertionError(repeat)

    # A ? on the end turns it into nongreedy, e.g. * -> *?
    # possessive translates to +, but Python doesn't support that.
    suffix2 = {'N': '?', 'P': False, None: ''}.get(modifier)
    if suffix2 is False:
      # TODO: need some context for these errors.
      raise Error("Modifier %r isn't supported in Python" % modifier)

    return gen_re(defs, child) + suffix1 + suffix2

  elif node_type == 'Seq':
    children = expr[1:]
    assert children
    return ''.join(gen_re(defs, c) for c in children)

  elif node_type == 'Choice':
    children = expr[1:]
    assert children
    return '|'.join(gen_re(defs, c) for c in children)

  elif node_type == 'Capture':
    name = expr[1]
    child = expr[2]
    r = gen_re(defs, child)
    if name:
      return '(?P<%s>%s)' % (name, r)
    else:
      return '(%s)' % r

  elif node_type == 'Group':
    r = gen_re(defs, child)
    return '(?:%s)' % r

  # ATOMS
  elif node_type == 'Literal':
    value = expr[1]
    return re.escape(value)

  elif node_type == 'Class':
    children = expr[1:-1]  # last one is whether it's negated
    parts = []
    for c in children:
      if isinstance(c, basestring):  # raw character
        # TODO: Use a modified escape function for char classes.
        part = _CharClassEscape(c)
      else:
        part = gen_re(defs, c)
      parts.append(part)
    neg = '' if expr[-1] else '^'
    return '[%s%s]' % (neg, ''.join(parts))

  elif node_type == 'Range':
    start, end = expr[1:]
    # TODO: need escaping
    return '%s-%s' % (start, end)

  elif node_type == 'Single':
    return SINGLE[name]
  elif node_type == 'Zero':
    return ZERO[name]

  # Character classes
  elif node_type == 'Perl':
    return PERL[name]
  elif node_type == 'Posix':
    # TODO:
    # - posix should get: '[:%s:]' % name
    # - we COULD support these in Python if we wanted.
    raise Error("Python doesn't support POSIX character classes: %s", expr)
  elif node_type == 'Unicode':
    raise Error("Python doesn't support POSIX character classes: %s", expr)

  # Backtracking
  elif node_type == 'Assert':
    child, left, positive = expr[1:4]
    chars = _ASSERT_CHARS[left, positive]
    return '(?%s%s)' % (chars, gen_re(defs, child))

  elif node_type == 'BackRef':
    if isinstance(name, int):
      return r'\%d' % name
    elif isinstance(name, basestring):
      return r'(?P=%s)' % name
    else:
      raise AssertionError(name)
    return '(?%s%s)' % (chars, gen_re(defs, child))

  else:
    raise AssertionError('Unknown node type %s' % node_type)

